<?php

/*
 * Copyright 2012, Serge V. Izmaylov
 * Released under GPL Version 2 license.
 */

define("JSONSS_SERVICE", 1);
define("JSONSS_METHOD", 2);
define("JSONSS_PROPERTY", 3);
define("JSONSS_EVENT", 4);
define("JSONSS_INTERFACE", 5);
define("JSONSS_FAULT", 6);
define("JSONSS_RESPONSE", 7);

define("JSONSS_METHOD_GET", "GET");
define("JSONSS_METHOD_SET", "SET");
define("JSONSS_METHOD_CALL", "CALL");
define("JSONSS_METHOD_PING", "PING");

define("JSONSS_NAME_RX", "/^[A-Za-z_]{1}[A-Za-z_0-9]*$/");
define("JSONSS_URLNAME_RX", "/^[A-Za-z_0-9~!:\\.,;()[\\]{}\\-]+$/");

class JSONSSError extends Exception {

    public $data;

    public function __construct($c, $m, $d = NULL) {
        if (!isset($c) || is_null($c) || !is_numeric($c))
            $c = 500;
        if (!isset($m) || is_null($m) || !is_string($m))
            $m = "Unspecified error";
        if (!isset($d))
            $d = NULL;
        parent::__construct($m, $c);
        $this->code = $c;
        $this->message = $m;
        $this->data = $d;
    }
}

class __JSONSSInterfaceItem {

    public $type;
    public $name;
    public $urlname;
    public $getter;
    public $setter;

    public function __construct($t, $n, $u = "", $g = "", $s = "") {
        if (!isset($n) || is_null($n) || !is_string($n))
            throw new JSONSSError(500, "Name of interface item must be string");
        if (!preg_match(JSONSS_NAME_RX, $n))
            throw new JSONSSError(500, "'" . $n . "' is invalid name of interface item");
        if (!isset($t) || is_null($t) || !is_int($t))
            throw new JSONSSError(500, "Type of of interface item '" . $n . "' must be int" );
        if (!in_array($t, array(JSONSS_SERVICE, JSONSS_METHOD, JSONSS_PROPERTY, JSONSS_EVENT)))
            throw new JSONSSError(500, "Invalid type of of interface item '" . $n . "': " . $t);
        if (!isset($u) || is_null($u) || !is_string($u))
            throw new JSONSSError(500, "Urlname of interface item '" . $n . "' must be string");
        if ($u == "")
            $u = $n;
        if (!preg_match(JSONSS_URLNAME_RX, $u))
            throw new JSONSSError(500, "'" . $u . "' is invalid urlname for interface item '" . $n . "'");
        if (!isset($g) || is_null($g) || !is_string($g))
            throw new JSONSSError(500, "Name of getter of interface item '" . $n . "' must be string");
        if (!empty($g) && !preg_match(JSONSS_NAME_RX, $g))
            throw new JSONSSError(500, "'" . $g . "' is invalid name of getter for interface item '" . $n . "'");
        if (!isset($s) || is_null($s) || !is_string($s))
            throw new JSONSSError(500, "Name of setter of interface item '" . $n . "' must be string");
        if (!empty($s) && !preg_match(JSONSS_NAME_RX, $s))
            throw new JSONSSError(500, "'" . $s . "' is invalid name of getter for interface item '" . $n . "'");
        $this->type = $t;
        $this->name = $n;
        $this->urlname = $u;
        $this->getter = $g;
        $this->setter = $s;
    }
}

class JSONSSService {

    private $interface = array();
    private $events = array();

    private function __checknameconflict($ii) {
        foreach ($this->interface as $k => $v) {
            if ($ii->name == $v->name)
                throw new JSONSSError(500, "Name '" . $ii->name . "' already exists in service");
            if ($ii->urlname == $v->urlname)
                throw new JSONSSError(500, "Urlname '" . $ii->urlname . "' already exists in service");
        }
    }

    public function add_service($name, $urlname) {
        $ii = new __JSONSSInterfaceItem(JSONSS_SERVICE, $name, $urlname);
        $this->__checknameconflict($ii);
        $this->interface[$ii->urlname] = $ii;
    }

    public function add_method($name, $urlname) {
        $ii = new __JSONSSInterfaceItem(JSONSS_METHOD, $name, $urlname);
        $this->__checknameconflict($ii);
        $this->interface[$ii->urlname] = $ii;
    }

    public function add_property($name, $urlname, $getter, $setter) {
        $ii = new __JSONSSInterfaceItem(JSONSS_PROPERTY, $name, $urlname, $getter, $setter);
        $this->__checknameconflict($ii);
        $this->interface[$ii->urlname] = $ii;
    }

    public function add_event($name) {
        $ii = new __JSONSSInterfaceItem(JSONSS_EVENT, $name);
        $this->__checknameconflict($ii);
        $this->interface[$ii->urlname] = $ii;
    }

    public function fire_event($evn, $data = NULL) {
        if (!array_key_exists($evn, $this->interface) || !isset($this->interface[$evn]) ||
            !is_a($this->interface[$evn], "__JSONSSInterfaceItem") || !isset($this->interface[$evn]->type)
            || ($this->interface[$evn]->type != JSONSS_EVENT)) {
            //throw new JSONSSError(500, "Unknown event '" . $evn . "' in '" . $request->service_item . "'");
            return;
        }
        $evt = new stdClass();
        $evt->n = $evn;
        $evt->d = $data;
        $this->response->events[] = $evt;
    }

    public function __ping(&$request, &$response) {
        $this->request = &$request;
        $this->response = &$response;
        $this->ping();
    }

    public function ping() {
        // ancestors may fire events
    }

    public function describe_interface() {
        $res = array();
        foreach ($this->interface as $k => $v) {
            $item = new stdClass();
            $item->t = $v->type;
            $item->n = $v->name;
            $item->u = $v->urlname;
            if ($v->type == JSONSS_SERVICE)
                $item->i = $this->{$v->name}->describe_interface();
            $res[] = $item;
        }
        return $res;
    }

    public function __dispatch($path_parts, &$request, &$response) {
        $this->request = &$request;
        $this->response = &$response;
        $pathitem = array_shift($path_parts);
        if (count($path_parts)) {
            if (!array_key_exists($pathitem, $this->interface) || !isset($this->interface[$pathitem]) ||
                !is_a($this->interface[$pathitem], "__JSONSSInterfaceItem") || !isset($this->interface[$pathitem]->name) ||
                !isset($this->interface[$pathitem]->type) || ($this->interface[$pathitem]->type != JSONSS_SERVICE)) {
                throw new JSONSSError(500, "Unknown interface '" . $pathitem . "' in '" . $request->service_item . "'");
            }
            $sn = $this->interface[$pathitem]->name;
            if (!isset($this->$sn) || !is_a($this->$sn, "JSONSSService")) {
                throw new JSONSSError(500, "Can't find service object '" . $sn . "' for '" . $request->service_item . "'");
            }
            return $this->$sn->__dispatch($path_parts, $request, $response);
        } else {
            if (!array_key_exists($pathitem, $this->interface) || !isset($this->interface[$pathitem]) ||
                !is_a($this->interface[$pathitem], "__JSONSSInterfaceItem") || !isset($this->interface[$pathitem]->name) ||
                !isset($this->interface[$pathitem]->type) || (($this->interface[$pathitem]->type != JSONSS_METHOD) &&
                ($this->interface[$pathitem]->type != JSONSS_PROPERTY) && ($this->interface[$pathitem]->type != JSONSS_SERVICE))) {
                throw new JSONSSError(500, "Unknown interface '" . $pathitem . "' in '" . $request->service_item . "'");
            }
            $ii = $this->interface[$pathitem];
            switch ($ii->type) {
                case JSONSS_METHOD:
                    if ($request->method == JSONSS_METHOD_CALL)
                        $this->__call_method($ii);
                    else
                        throw new JSONSSError(500, "Bad JSONSS method '" . $request->method . "' for method '" . $ii->name . "'");
                    break;
                case JSONSS_PROPERTY:
                    if ($request->method == JSONSS_METHOD_GET)
                        $this->__get_property($ii);
                    elseif ($request->method == JSONSS_METHOD_SET)
                        $this->__set_property($ii);
                    else
                        throw new JSONSSError(500, "Bad JSONSS method '" . $request->method . "' for property '" . $ii->name . "'");
                    break;
                case JSONSS_SERVICE:
                    if ($request->method == JSONSS_METHOD_PING)
                        $this->__ping_service($ii);
                    else
                        throw new JSONSSError(500, "Bad JSONSS method '" . $request->method . "' for service '" . $ii->name . "'");
                    break;
                default:
                    throw new JSONSSError(500, "Unknown interface item '" . $ii->name . "' on dispatch");
            }
        }
    }

    private function __call_method($ii) {
        if (!method_exists($this, $ii->name))
            throw new JSONSSError(500, "Can't find method '" . $ii->name . "' for '" . $this->request->service_item . "'");
        $this->response->return = $this->{$ii->name}($this->request->param);
    }

    private function __get_property($ii) {
        if (!isset($ii->getter) || empty($ii->getter) || !method_exists($this, $ii->getter))
            throw new JSONSSError(500, "No getter for '" . $ii->name . "' in '" . $this->request->service_item . "'");
        $this->response->return = $this->{$ii->getter}($this->request->param);
    }

    private function __set_property($ii) {
        if (!isset($ii->setter) || empty($ii->setter) || !method_exists($this, $ii->setter))
            throw new JSONSSError(500, "No setter for '" . $ii->name . "' in '" . $this->request->service_item . "'");
        $this->{$ii->setter}($this->request->param);
    }

    private function __ping_service($ii) {
        if (!property_exists($this, $ii->name) || !is_a($this->{$ii->name}, "JSONSSService"))
            throw new JSONSSError(500, "No service for '" . $ii->name . "' in '" . $this->request->service_item . "'");
        $this->{$ii->name}->__ping($this->request, $this->response);
    }
}

class JSONSSServer {

    private $root_service;

    public function __construct() {
        $this->root_service = new JSONSSService();
    }

    public function process_request() {
        if (!isset($_SERVER["REQUEST_METHOD"])) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error(new JSONSSError(500, "No REQUEST_METHOD"));
            echo json_encode($res);
            return;
        }
        switch ($_SERVER["REQUEST_METHOD"]) {
            case "GET":
                $this->__process_GET();
                break;
            case "POST":
                $this->__process_POST();
                break;
            default:
                $this->__process_Unknown();
                break;
        }
    }

    public function add_service($object, $name, $urlname = "") {
        if (!is_object($object) || !is_a($object, "JSONSSService"))
            throw new JSONSSError(500, "Service instance must be object of class JSONSSService");
        if (!isset($name) || is_null($name) || !is_string($name))
            throw new JSONSSError(500, "Service name be string");
        if (!preg_match(JSONSS_NAME_RX, $name))
            throw new JSONSSError(500, "'" . $name . "' is invalid name for service");
        $this->root_service->add_service($name, $urlname);
        $this->root_service->$name = $object;
    }

    private function __process_GET() {
        $res = new stdClass();
        try {
            $res->t = JSONSS_INTERFACE; 
            $res->i = $this->root_service->describe_interface();
        } catch (Exception $x) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error($x);
        }
        echo json_encode($res);
    }

    private function __process_POST() {
        $res = new stdClass();

        if (!isset($_SERVER["HTTP_JSONSSMETHOD"])) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error(new JSONSSError(500, "Need JSONSSMethod header"));
            echo json_encode($res);
            return;
        }
        if (!isset($_SERVER["PATH_INFO"])) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error(new JSONSSError(500, "Need service path"));
            echo json_encode($res);
            return;
        }

        $req = new stdClass();
        try {
            $req->method = "" . $_SERVER["HTTP_JSONSSMETHOD"];
            $req->text = file_get_contents("php://input");
            $req->service_item = $_SERVER["PATH_INFO"];
            $req->param = json_decode($req->text);
        } catch (Exception $x) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error($x);
            echo json_encode($res);
            return;
        }

        $resp = new stdClass();
        $resp->return = NULL;
        $resp->events = array();

        $path_parts = explode("/", $_SERVER["PATH_INFO"]);
        if (empty($path_parts[0]))
            array_shift($path_parts);
        if (empty($path_parts) || empty($path_parts[0])) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error(new JSONSSError(500, "No service path"));
            echo json_encode($res);
            return;
        }
        try {
            $this->root_service->__dispatch($path_parts, $req, $resp);
        } catch (Exception $x) {
            $res->t = JSONSS_FAULT;
            $res->d = self::__strip_error($x);
            echo json_encode($res);
            return;
        }

        $res->t = JSONSS_RESPONSE;
        $res->r = $resp->return;
        $res->e = $resp->events;
        echo json_encode($res);
    }

    private function __process_Unknown() {
        $res->t = JSONSS_FAULT;
        $res->d = self::__strip_error(new JSONSSError(500, "Unknown request method '" . $_SERVER["REQUEST_METHOD"] . "'"));
        echo json_encode($res);
    }

    private static function __strip_error($x) {
        $res = new stdClass();
        if (!is_a($x, "JSONSSError")) {
            $res->c = 500;
            $res->m = "Internal error: " . $x->getMessage();
            $res->d = NULL;
        } else {
            $res->c = (int)($x->getCode());
            $res->m = $x->getMessage();
            $res->d = $x->data;
        }
        return $res;
    }
}
